﻿using System.Collections;
using System.Collections.Generic;
using UnityEngine;

namespace Utility.NodeGraphs
{
	public class Node : ScriptableObject
	{
		public enum PortOnMaxConnectionBehaviours
		{
			IgnoreNewConnection,
			OverrideLastConnection,
			OverrideFirstConnection,
		}

		public NodeGraph graph;
		public Vector2 position;

		public List<NodePort> nodePorts = new List<NodePort> ();

		public Vector2 CanvasPos => position + graph.position;

		public virtual PortOnMaxConnectionBehaviours GetPortOnMaxConnectionBehaviour (int index) => PortOnMaxConnectionBehaviours.IgnoreNewConnection;
		public virtual bool PortAllowBidirectionalConnection (int index) => false;
		public virtual bool PortAllowConnectToOwnerNode (int index) => false;
		public virtual bool PortAllowConnectFromOwnerNode (int index) => false;

		public virtual bool CanAddPort () => true;
		public virtual bool CanRemovePort (int index) => true;

		public virtual bool PortCanConnectTo (int index, NodePort targetPort)
		{
			NodePort port = nodePorts[index];
			//can not connect to self
			if (targetPort == port)
			{
				return false;
			}
			//check owner node
			if (targetPort.Node == this && !PortAllowConnectToOwnerNode (index))
			{
				return false;
			}
			//check output limit
			if (port.ReachOutputLimit)
			{
				return false;
			}
			//check duplicated connection
			if (port.HasConnectionTo (targetPort))
			{
				return false;
			}
			//check bidirectional
			if (!PortAllowBidirectionalConnection (index))
			{
				if (targetPort.HasConnectionTo (this))
				{
					return false;
				}
			}
			return true;
		}

		public virtual bool PortCanConnectFrom (int index, NodePort sourcePort)
		{
			NodePort port = nodePorts[index];
			//can not connect to self
			if (sourcePort == port)
			{
				return false;
			}
			//check owner node
			if (sourcePort.Node == this && !PortAllowConnectFromOwnerNode (index))
			{
				return false;
			}
			//check input limie
			if (port.ReachInputLimit)
			{
				return false;
			}
			//check duplicated connection
			if (port.HasConnectionFrom (sourcePort))
			{
				return false;
			}//check bidirectional
			if (!PortAllowBidirectionalConnection (index))
			{
				if (sourcePort.HasConnectionFrom (this))
				{
					return false;
				}
			}
			return true;
		}

		private void Awake ()
		{
			OnAwake ();
		}

		public virtual void OnAwake ()
		{
			if (nodePorts.Count == 0)
			{
				AddNewPort ();
			}
		}

		public virtual void OnRemove ()
		{
			foreach (var port in nodePorts)
			{
				port.RemoveConnections ();
			}
		}

		public virtual void OnConnectionsChange () { }
		public virtual void OnPortsChange () { }

		#region port
		public virtual NodePort AddNewPort ()
		{
			if (!CanAddPort ())
			{
				return null;
			}

			NodePort port = NodePort.New (this);
			nodePorts.Add (port);

			//caalback
			OnPortsChange ();
			return port;
		}

		public virtual NodePort AddNewPort (int inputLimit, int outputLimit, Vector2 dirPoint = default)
		{
			if (!CanAddPort ())
			{
				return null;
			}

			var port = AddNewPort ();
			port.inputLimit = inputLimit;
			port.outputLimit = outputLimit;
			port.dirPoint = dirPoint;
			return port;
		}

		public virtual NodePort InsertNewPort (int index)
		{
			if (!CanAddPort ())
			{
				return null;
			}

			NodePort port = NodePort.New (this);
			for (int i = index; i < nodePorts.Count; i++)
			{
				nodePorts[i].OnChangeIndex (i + 1);
			}
			nodePorts.Insert (index, port);
			//caalback
			OnPortsChange ();
			return port;
		}

		public virtual NodePort InsertNewPort (int index, int inputLimit, int outputLimit, Vector2 dirPoint = default)
		{
			if (!CanAddPort ())
			{
				return null;
			}

			var port = InsertNewPort (index);
			port.inputLimit = inputLimit;
			port.outputLimit = outputLimit;
			port.dirPoint = dirPoint;
			return port;
		}

		public virtual void RemovePort (int index)
		{
			if (index < 0 || index >= nodePorts.Count)
			{
				return;
			}
			if (!CanRemovePort (index))
			{
				return;
			}

			//remove port connections
			NodePort port = nodePorts[index];
			port.RemoveConnections ();

			//change the index reference for other port connections
			for (int i = index + 1; i < nodePorts.Count; i++)
			{
				nodePorts[i].OnChangeIndex (i - 1);
			}

			//remove the port
			nodePorts.RemoveAt (index);

			//callback
			OnPortsChange ();
		}

		public void RemovePort (NodePort port)
		{
			RemovePort (GetPortIndex (port));
		}

		public NodePort GetPort (string name)
		{
			return nodePorts.Find (port => port.name == name);
		}

		public NodePort GetPort (int index)
		{
			if (index < 0 || index >= nodePorts.Count)
			{
				return null;
			}
			return nodePorts[index];
		}

		public int GetPortIndex (NodePort port)
		{
			return nodePorts.FindIndex (p => p == port);
		}
		#endregion
		#region connections
		public void RemoveAllInputs ()
		{
			foreach (var port in nodePorts)
			{
				port.RemoveInputs ();
			}
		}

		public void RemoveAllOutputs ()
		{
			foreach (var port in nodePorts)
			{
				port.RemoveOutputs ();
			}
		}

		public void RemoveAllConnections ()
		{
			foreach (var port in nodePorts)
			{
				port.RemoveConnections ();
			}
		}
		#endregion
	}
}